/*

 ActorX mesh (psk) and animation (psa) importer for 3ds Max

 Created:	September 18 2009

 Author:	Konstantin Nosov (aka Gildor)

 Web page:	http://www.gildor.org/projects/unactorx

 Revision History:

	21.07.2015 v1.33
	- saving bind pose information inside bone objects, this information will survive saving scene
	  to a max file

	18.07.2015 v1.32
	- allowing psa import to work with any mesh, i.e. without previously imported psk file

	14.07.2015 v1.31
	- trying to detect and repair bad vertex weights for imported psk file

	01.02.2015 v1.30
	- added animation import option "import at slider position"
	- integrated patch by Ayrshi (http://www.gildor.org/smf/index.php/topic,1925.0.html) intended to
	  improve mesh normals

	02.12.2014 v1.29
	- ActorX Imported now could be bound to toolbar, keyboard or menu - use "Customize user interface",
	  category "Gildor Tools", and then use "ActorX Importer" as you like
	- reordered controls, separated some options to own rollouts for easy reordering etc
	- preserving dialog position, scroll position and rollout "open" state during 3ds Max session (until
	  Max closed)

	28.11.2014 v1.28
	- added "mesh translation" options to settings
	- "advanced settings" are not stored to the ini file anymore

	16.12.2013 v1.27
	- added option "Don't conjugate root bone"

	10.06.2012 v1.26
	- stability improvements
	  more info: MaxScript documentation, "Do not use return, break, exit or continue"

	02.06.2012 v1.25
	- fixed Max 2013 support; fix made by sunnydavis, check
	  http://www.gildor.org/smf/index.php/topic,1408.0.html for details

	18.02.2012 v1.24
	- fixed parsing psa config file with spaces in track names

	07.02.2012 v1.23
	- support for extra UV sets stored in standard psk format (ActorX 2010)

	23.01.2012 v1.22
	- fixed automatic loading of DDS textures for materials

	06.12.2011 v1.21
	- fixed "translation mode" checkbox to work with psa without config file

	01.12.2011 v1.20
	- implemented loading of DDS textures

	26.11.2011 v1.19
	- implemented support for loading pskx files with more than 64k vertices
	- added option to control behaviour of animation with rotation-only tracks: you can let AnimSet
	  to decide which bones will use animated translation, you can force to use translation from the
	  animation (old, pre-1.18 behaviour) or force to not use animated translation at all; the option
	  is located in "Animation import" group

	09.11.2011 v1.18
	- implemented support for animation tracks without translation keys
	- reading extended psa information from the .config file, removed psax ANIMFLAGS section support

	06.11.2011 v1.17
	- eliminated error messages when loading psk or psa file with unknown section name (SCALEKEYS etc)
	- implemented support for pskx with 2 or more UV channels

	03.05.2011 v1.16
	- improved animation cleanup

	01.01.2011 v1.15
	- workaround for loading animation with the root bone name different than mesh root bone
	- removed "Load confirmation" setting (not needed anymore because of functional "batch export")

	29.12.2010 v1.14
	- added "Batch export" tool

	22.12.2010 v1.13
	- mesh rotation formula is now identical to used in UnrealEd
	- added "Clear scene" tool

	15.12.2010 v1.12
	- added mesh rotation settings
	- added protection from errors appeared when updating this script while 3ds Max is running

	09.09.2010 v1.11
	- added "reorient bones" option

	23.07.2010 v1.10
	- implemented extended ActorX format (pskx and psax) support
	- "tools" rollout with options to restore mesh bindpose and remove animations

	24.04.2010 v1.09
	- applying normalmap using correct technique (previously was a bumpmap)

	14.04.2010 v1.08
	- fixed loading of psk files with root bone parent set to -1 (usually it is 0)

	20.02.2010 v1.07
	- added "Load confirmation" setting to display message box after completion of operation
	- added "Reposition existing bones" option
	- fixed error when loading .mat files with missing textures

	12.12.2009 v1.06
	- fixed merging meshes on a single skeleton when previously loaded mesh is not in bind
	  pose
	- improved compatibility with Epic's ActorX Exporter (dropping trailing spaces from
	  bone names)

	18.09.2009 v1.05
	- implemented materal loading
	- fixing duplicate bone names

	29.09.2009 v1.04
	- implemented support for loading non-skeletal (static) meshes

	26.09.2009 v1.03
	- fixed bug with interpolation between first two animation keyframes
	- option to fix animation looping (duplicate first animation frame after last frame)
	- added button to load all animations from psa file
	- progress bar for loading animation with "cancel" capabilities
	- option to not load mesh skin (load skeleton only)
	- storing last used directory separately for psk and psa

	25.09.2009 v1.02
	- added option to scale mesh and animations when loading
	- added options for texture search (path, recursive search)
	- added option to ask for missing texture files when mesh is loading

	24.09.2009 v1.01
	- fixed bug in a vertex weighting code
	- saving settings to ActorXImporter.ini (Max 9 and higher)
	- saving last used psk/psa directory
	- settings to change bone size for a new mesh

	22.09.2009 v1.00
	- first public release

*/


/*
TODO:
- option to create separate materials, not submaterials
- do not create material when it is already exists - but how to find whether I need to get loaded material
  or create a new one?
*/

-- constant used to detect ActorX Importer updates during single 3ds Max session
global AX_IMPORTER_VERSION = 133

-------------------------------------------------------------------------------
--	Global variables
-------------------------------------------------------------------------------

global g_seeThru
global g_skelOnly
global g_updateTime
global g_playAnim
global g_animAtSlider
global g_animTransMode			-- 1 = from AnimSet, 2 = force mesh translation, 3 = force AnimSet translation
global g_fixLooping
global g_lastDir1
global g_lastDir2
global g_texDir
global g_texRecurse
global g_texMissAction
global g_boneSize
global g_reposBones
global g_rotY
global g_rotP
global g_rotR
global g_transX
global g_transY
global g_transZ
global g_meshScale
global g_reorientBones
global g_dontConjugateRoot
global Anims					-- array of AnimInfoBinary


-------------------------------------------------------------------------------
--	Default settings
-------------------------------------------------------------------------------

fn axDefaultSettings =
(
	-- defaults settings
	g_seeThru    = false
	g_skelOnly   = false
	g_updateTime = true
	g_playAnim   = false
	g_animAtSlider = false
	g_animTransMode = 1
	g_fixLooping = false
	g_lastDir1   = ""
	g_lastDir2   = ""
	g_texDir     = ""
	g_texRecurse = true
	g_texMissAction = 1
	g_boneSize   = 0.5
	g_reposBones = true
	g_rotY       = 0
	g_rotP       = 0
	g_rotR       = 0
	g_transX     = 0
	g_transY     = 0
	g_transZ     = 0
	g_meshScale  = 1.0
	g_reorientBones = false
	g_dontConjugateRoot = false
)


-------------------------------------------------------------------------------
--	Configuration
-------------------------------------------------------------------------------

configFile = undefined
if getSourceFileName != undefined then	-- checking Max version (Max9+) ...
(
	local s = getSourceFileName()
	configFile = (getFilenamePath s) + (getFilenameFile s) + ".ini"
)


tmp_v = undefined		-- global variable, helper for axDoSetting() (required for execute() ...)
g_isLoading = true		-- axDoSetting() mode

fn axDoSetting name var =
(
	local default = execute var							-- value has the same type as var
	if g_isLoading then
	(
		try
		(
			-- loading value
			tmp_v = getINISetting configFile "Main" name	-- get from ini as string
			if (tmp_v != "") and (tmp_v != "undefined") then
			(
				local type = classOf default
--				format "reading % (%) = %\n" var type tmp_v
				if (not isKindOf default String) then
					execute (var + "=tmp_v as " + (type as string))
				else
					execute (var + "=tmp_v")				-- no conversion
			)
		)
		catch
		(
			format "Reading %: %\n" name (getCurrentException())
		)
	)
	else
	(
		-- saving value
		setINISetting configFile "Main" name (default as string)
	)
)


fn axSerializeSettings isLoading =
(
	if isLoading then
	(
		if configFile == undefined then return undefined
		if not doesFileExist configFile then return undefined	-- no config file
	)
	g_isLoading = isLoading
	-- read/write settings
	axDoSetting "LastUsedDir"   "g_lastDir1"
	axDoSetting "LastUsedDir2"  "g_lastDir2"
	axDoSetting "TexturesDir"   "g_texDir"
	axDoSetting "TexRecurse"    "g_texRecurse"
	axDoSetting "TexMissAction" "g_texMissAction"
	axDoSetting "AutoPlayAnim"  "g_playAnim"
	axDoSetting "AnimAtSlider"  "g_animAtSlider"
	axDoSetting "AnimTransMode" "g_animTransMode"
	axDoSetting "UpdateTime"    "g_updateTime"
	axDoSetting "FixLoopAnim"   "g_fixLooping"
	axDoSetting "SeeThru"       "g_seeThru"
	axDoSetting "SkelOnly"      "g_skelOnly"
	axDoSetting "BoneSize"      "g_boneSize"
	axDoSetting "ReposBones"    "g_reposBones"
	axDoSetting "MeshYaw"       "g_rotY"
	axDoSetting "MeshPitch"     "g_rotP"
	axDoSetting "MeshRoll"      "g_rotR"
	axDoSetting "MeshX"         "g_transX"
	axDoSetting "MeshY"         "g_transY"
	axDoSetting "MeshZ"         "g_transZ"
	axDoSetting "MeshScale"     "g_meshScale"
--	axDoSetting "ReorientBones" "g_reorientBones"
--	axDoSetting "DontConjRoot"  "g_dontConjugateRoot"
)


-------------------------------------------------------------------------------
--	Service functions
-------------------------------------------------------------------------------

fn ErrorMessage text =
(
	local msg = ("ERROR: " + text + "\n")
	format "%\n" msg
	messageBox msg
	throw msg
)


fn TrimSpaces text =
(
	trimLeft(trimRight(text))
)


fn IsEndOfFile bstream =
(
	local savePos = ftell bstream
	fseek bstream 0 #seek_end			-- compute file size
	local fileSize = ftell bstream
	fseek bstream savePos #seek_set
	(savePos >= fileSize)
)


fn ReadFixedString bstream fixedLen =
(
	local str = ""
	local length = 0
	local finished = false
	for i = 1 to fixedLen do
	(
		local c = ReadByte bstream #unsigned
		if c == 0 then finished = true	-- end of line char
		if not finished then 			-- has end of line before - skip remaining chars
		(
			-- not "finished" string
			str += bit.intAsChar(c)		-- append a character
			if c != 32 then length = i	-- position of last non-space char
		)
	)
	substring str 1 length				-- return first "length" chars
)

fn ReadVector2 bstream =
(
	local v = point2 0 0
	v.x = ReadFloat bstream
	v.y = ReadFloat bstream
	v
)

fn ReadFVector bstream =
(
	local v = point3 0 0 0
	v.x = ReadFloat bstream
	v.y = ReadFloat bstream
	v.z = ReadFloat bstream
	v
)

fn ReadFQuat bstream =
(
	local q = quat 0 0 0 0
	q.x = ReadFloat bstream
	q.y = ReadFloat bstream
	q.z = ReadFloat bstream
	q.w = ReadFloat bstream
	q
)

-- Function used to determine bone length
fn axFindFirstChild boneArray boneIndex =
(
	local res = undefined, notfound = true
	for i = 1 to boneArray.count while notfound do
	(
		if (i != boneIndex) then
		(
			bn = boneArray[i]
			if bn.ParentIndex == boneIndex-1 then
			(
				res = bn
				notfound = false
			)
		)
	)
	res
)


fn axFixBoneNames boneArray =
(
	-- Find and correct duplicate names
	for i = 1 to (boneArray.count-1) do
	(
		local n = boneArray[i].Name
		local dupCount = 1
		for j = (i+1) to boneArray.count do
		(
			local n2 = boneArray[j].Name
			if n == n2 then
			(
				dupCount += 1
				n2 = n + "_" + (dupCount as string)
				format "Duplicate bone name \"%\", renamed to \"%\"\n" n n2
				boneArray[j].Name = n2
			)
		)
	)
)


fn axFindFile path filename recurse:false =
(
	local res = undefined
	local check = path + "\\" + filename
	if doesFileExist check then
	(
		res = check
	)
	else if recurse then
	(
		local dirs = getDirectories (path + "/*")
		local notfound = true
		for dir in dirs while notfound do
		(
			res = axFindFile dir filename recurse:true
			if res != undefined then
			(
				notfound = false		-- break the loop
			)
		)
	)
	res
)


fn axGetRootMatrix =
(
	local angles = eulerAngles g_rotR -g_rotP -g_rotY
	local m = angles as matrix3
	m.translation = [g_transX, g_transY, g_transZ]
	m
)


-------------------------------------------------------------------------------
--	ActorX data structures
-------------------------------------------------------------------------------

struct VChunkHeader
(
	ChunkID,
	TypeFlag,
	DataSize,
	DataCount
)

fn ReadChunkHeader bstream =
(
	local hdr = VChunkHeader ()
	hdr.ChunkID   = ReadFixedString bstream 20
	hdr.TypeFlag  = ReadLong bstream #unsigned
	hdr.DataSize  = ReadLong bstream #unsigned
	hdr.DataCount = ReadLong bstream #unsigned
--	format "Read chunk header: %\n" hdr
	hdr
)

struct VVertex
(
	PointIndex,
	U, V,
	MatIndex,
	Reserved,
	Pad
)

fn ReadVVertex bstream =
(
	local v = VVertex ()
	local pad
	v.PointIndex = ReadShort bstream #unsigned
	pad          = ReadShort bstream
	v.U          = ReadFloat bstream
	v.V          = ReadFloat bstream
	v.MatIndex   = ReadByte  bstream #unsigned
	v.Reserved   = ReadByte  bstream #unsigned
	v.Pad        = ReadShort bstream #unsigned
	v
)

fn ReadVVertex32 bstream =
(
	local v = VVertex ()
	v.PointIndex = ReadLong  bstream #unsigned			-- short -> long, no "pad"
	v.U          = ReadFloat bstream
	v.V          = ReadFloat bstream
	v.MatIndex   = ReadByte  bstream #unsigned
	v.Reserved   = ReadByte  bstream #unsigned
	v.Pad        = ReadShort bstream #unsigned
	v
)

struct VTriangle
(
	Wedge0, Wedge1, Wedge2,
	MatIndex,
	AuxMatIndex,
	SmoothingGroups
)

fn ReadVTriangle bstream =
(
	local v = VTriangle ()
	v.Wedge0          = ReadShort bstream #unsigned
	v.Wedge1          = ReadShort bstream #unsigned
	v.Wedge2          = ReadShort bstream #unsigned
	v.MatIndex        = ReadByte  bstream #unsigned
	v.AuxMatIndex     = ReadByte  bstream #unsigned
	v.SmoothingGroups = ReadLong  bstream #unsigned
	v
)

fn ReadVTriangle32 bstream =
(
	local v = VTriangle ()
	v.Wedge0          = ReadLong  bstream #unsigned		-- short -> long
	v.Wedge1          = ReadLong  bstream #unsigned		-- ...
	v.Wedge2          = ReadLong  bstream #unsigned		-- ...
	v.MatIndex        = ReadByte  bstream #unsigned
	v.AuxMatIndex     = ReadByte  bstream #unsigned
	v.SmoothingGroups = ReadLong  bstream #unsigned
	v
)

struct VMaterial
(
	MaterialName,
	TextureIndex,
	PolyFlags,
	AuxMaterial,
	AuxFlags,
	LodBias,
	LodStyle
)

fn ReadVMaterial bstream =
(
	local m = VMaterial ()
	m.MaterialName = ReadFixedString bstream 64
	m.TextureIndex = ReadLong bstream #unsigned
	m.PolyFlags    = ReadLong bstream #unsigned
	m.AuxMaterial  = ReadLong bstream #unsigned
	m.AuxFlags     = ReadLong bstream #unsigned
	m.LodBias      = ReadLong bstream
	m.LodStyle     = ReadLong bstream
	m
)


struct VBone
(
	Name,
	Flags,
	NumChildren,
	ParentIndex,
	-- VJointPos
	Orientation,
	Position,
	Length,
	Size,
	-- Computed data
	Matrix
)

fn ReadVBone bstream =
(
	local b = VBone ()
	b.Name        = ReadFixedString bstream 64
	b.Flags       = ReadLong    bstream #unsigned
	b.NumChildren = ReadLong    bstream
	b.ParentIndex = ReadLong    bstream
	b.Orientation = ReadFQuat   bstream
	b.Position    = ReadFVector bstream
	b.Length      = ReadFloat   bstream
	b.Size        = ReadFVector bstream
	b
)


struct VRawBoneInfluence
(
	Weight,
	PointIndex,
	BoneIndex
)

fn ReadVRawBoneInfluence bstream =
(
	local v = VRawBoneInfluence ()
	v.Weight     = ReadFloat bstream
	v.PointIndex = ReadLong bstream #unsigned
	v.BoneIndex  = ReadLong bstream #unsigned
	v
)

fn InfluenceSort v1 v2 =
(
	local cmp = v1.PointIndex - v2.PointIndex
	if (cmp == 0) then cmp = v1.BoneIndex - v2.BoneIndex
	cmp
)


struct AnimInfoBinary
(
	Name,
	Group,
	TotalBones,
	RootInclude,
	KeyCompressionStyle,
	KeyQuotum,
	KeyReduction,
	TrackTime,
	AnimRate,
	StartBone,
	FirstRawFrame,
	NumRawFrames
)

fn ReadAnimInfoBinary bstream =
(
	v = AnimInfoBinary ()
	v.Name                = ReadFixedString bstream 64
	v.Group               = ReadFixedString bstream 64
	v.TotalBones          = ReadLong  bstream
	v.RootInclude         = ReadLong  bstream
	v.KeyCompressionStyle = ReadLong  bstream
	v.KeyQuotum           = ReadLong  bstream
	v.KeyReduction        = ReadFloat bstream
	v.TrackTime           = ReadFloat bstream
	v.AnimRate            = ReadFloat bstream
	v.StartBone           = ReadLong  bstream
	v.FirstRawFrame       = ReadLong  bstream
	v.NumRawFrames        = ReadLong  bstream
	v
)


struct VQuatAnimKey
(
	Position,
	Orientation,
	Time
)

fn ReadVQuatAnimKey bstream =
(
	local k = VQuatAnimKey ()
	k.Position    = ReadFVector bstream
	k.Orientation = ReadFQuat   bstream
	k.Time        = ReadFloat   bstream
	k
)


-------------------------------------------------------------------------------
--	Bone attributes
-------------------------------------------------------------------------------

AXBoneCustomDataDef = attributes AXBoneCustomData
attribID:#(0xF3DD7FCD, 0x4DB58449)
(
	parameters BindPose
	(
		AX_RelMatrix	type: #matrix3		-- matrix relative to parent bone
		AX_WorldMatrix	type: #matrix3		-- world matrix
	)
)

-------------------------------------------------------------------------------
--	Loading materials
-------------------------------------------------------------------------------

fn axFindTexture texDir baseName =
(
	-- DDS
	foundTex = axFindFile texDir (baseName + ".dds") recurse:g_texRecurse
	if foundTex == undefined then
	(
		-- TGA
		foundTex = axFindFile texDir (baseName + ".tga") recurse:g_texRecurse
/*		if foundTex == undefined then
		(
			-- other formats?
		) */
	)
	foundTex
)

fn axImportMaterial matName texDir =
(
	local subMat = standardMaterial name:matName

	local texFilename
	local foundTex

	-- try to file material file
	texFilename = matName + ".mat"
	foundTex = axFindFile texDir texFilename recurse:g_texRecurse
	if foundTex != undefined then
	(
		texFilename = foundTex
		format "Loading material %\n" texFilename
		local matFile = openFile texFilename
		while eof matFile == false do
		(
			local line = readline matFile
			local tok = filterString line " ="
--			format "[%] = [%]\n" tok[1] tok[2]
			local parm = tok[1]
			local file = tok[2]
			foundTex = axFindTexture texDir file
			if foundTex == undefined then continue
			local bitmap = bitmapTexture name:foundTex fileName:foundTex
			if parm == "Normal" then
			(
				local normalMap = normal_bump name:foundTex normal_map:bitmap
				subMat.bumpMap = normalMap
				subMat.bumpMapAmount = 100		-- amount is set to 30 by default
			)
			else
			(
				if parm == "Diffuse"   then subMat.diffuseMap = bitmap
				if parm == "Specular"  then subMat.specularMap = bitmap
				if parm == "SpecPower" then subMat.specularLevelMap = bitmap
				if parm == "Opacity"   then subMat.opacityMap = bitmap
				if parm == "Emissive"  then subMat.selfIllumMap = bitmap
			)
		)
		close matFile
		return subMat
	)
	-- no material file found, try simple texture
	-- get texture filename
	texFilename = matName
	foundTex = axFindTexture texDir matName
	if foundTex != undefined then
	(
		texFilename = foundTex
	)
	else
	(
		if g_texMissAction == 2 then			-- ask
		(
			local check = getOpenFileName caption:("Get texture for material " + matName) \
				types:"Texture files (*.tga,*.dds)|*.tga;*.dds|All (*.*)|*.*|" filename:texFilename
			if check != undefined then texFilename = check
		)
	)
	if not doesFileExist texFilename then format "Unable to find texture %\n" texFilename
	-- continue setup (even in a case of error)
	local bitmap = bitmapTexture name:texFilename fileName:texFilename
	subMat.diffuseMap = bitmap
	-- return
	subMat
)

-------------------------------------------------------------------------------
--	MAX helpers
-------------------------------------------------------------------------------

fn FindAllBones_Recurse bones parent =
(
	for i = 1 to parent.children.count do
	(
		node = parent.children[i]
		if isKindOf node BoneObj then
		(
			append bones node
		)
		FindAllBones_Recurse bones node
	)
)

fn FindAllBones =
(
	local bones = #()

	FindAllBones_Recurse bones rootNode

	bones
)

fn RemoveAnimation =
(
	stopAnimation()
	bones = FindAllBones()
	for i = 1 to bones.count do
	(
		b = bones[i]
		deleteKeys b #allKeys
	)
	animationRange = interval 0 1
)


fn RestoreBindpose =
(
	RemoveAnimation()

	try
	(
		local rotMatrix = axGetRootMatrix()
		-- note: should rotate every bone because we are not applying parent's rotation here
		-- find bones
		bones = FindAllBones()
		for i = 1 to bones.count do
		(
			b = bones[i]
			data = custAttributes.get b AXBoneCustomDataDef
			if data != undefined then
			(
				b.transform = data.AX_WorldMatrix * rotMatrix
			)
--			else
--			(
--				format "no info for %\n" b.name
--			)
		)

		set coordsys world
	)
	catch
	(
		format "ERROR!\n"
	)
)


fn ClearMaxScene =
(
	max select all
	if $ != undefined then delete $
)


-------------------------------------------------------------------------------
--	Loading PSK file
-------------------------------------------------------------------------------

fn ImportPskFile filename skelOnly:false =
(
	set coordsys world

	local Verts     = #()
	local Wedges    = #()
	local Tris      = #()
	local Materials = #()
	local MeshBones = #()
	local Infs      = #()

	--------- Read the file ---------

	local numVerts      = 0
	local numWedges     = 0
	local numTris       = 0
	local numMaterials  = 0
	local numBones      = 0
	local numInfluences = 0
	local numTexCoords  = 1

	local extraUV = #()

	local profileStartTime = timeStamp()

	try
	(
		file = fopen filename "rb"
		if file == undefined then return undefined

		-- First header --
		hdr = ReadChunkHeader file
		if (hdr.ChunkID != "ACTRHEAD") then
		(
			ErrorMessage("Bad chunk header: \"" + hdr.ChunkID + "\"")
		)

		while not IsEndOfFile(file) do
		(
			hdr = ReadChunkHeader file
			local chunkID = hdr.ChunkID
			-- check for extra UV set from latest ActorX exporter
			-- note: data has the same format as pskx extension, so the same loading code is used
			if (chunkID == "EXTRAUVS0") or (chunkID == "EXTRAUVS1") or (chunkID == "EXTRAUVS2") then
				chunkID = "EXTRAUV0";
--			format "Chunk: % (% items, % bytes/item, pos %)\n" hdr.ChunkID hdr.DataCount hdr.DataSize (ftell file)
			case chunkID of
			(
			-- Points --
			"PNTS0000":
				(
					numVerts = hdr.DataCount
					Verts[numVerts] = [ 0, 0, 0 ]		-- preallocate
					for i = 1 to numVerts do Verts[i] = ReadFVector file
				)

			-- Wedges --
			"VTXW0000":
				(
					numWedges = hdr.DataCount
					Wedges[numWedges] = VVertex ()		-- preallocate
					if numWedges <= 65536 then
					(
						for i = 1 to numWedges do Wedges[i] = ReadVVertex file
					)
					else
					(
						for i = 1 to numWedges do Wedges[i] = ReadVVertex32 file
					)
				)

			-- Faces --
			"FACE0000":
				(
					numTris = hdr.DataCount
					Tris[numTris] = VTriangle ()		-- preallocate
					for i = 1 to numTris do Tris[i] = ReadVTriangle file
				)

			-- Faces32 --
			"FACE3200":
				(
					numTris = hdr.DataCount
					Tris[numTris] = VTriangle ()		-- preallocate
					for i = 1 to numTris do Tris[i] = ReadVTriangle32 file
				)

			-- Materials --
			"MATT0000":
				(
					numMaterials = hdr.DataCount
					Materials[numMaterials] = VMaterial ()	-- preallocate
					for i = 1 to numMaterials do Materials[i] = ReadVMaterial file
				)

			-- Bones --
			"REFSKELT":
				(
					numBones = hdr.DataCount
					if numBones > 0 then MeshBones[numBones] = VBone () -- preallocate
					for i = 1 to numBones do
					(
						MeshBones[i] = ReadVBone file
--							format "Bone[%] = %\n" (i-1) MeshBones[i].Name
					)
					axFixBoneNames MeshBones
				)

			-- Weights --
			"RAWWEIGHTS":
				(
					numInfluences = hdr.DataCount
					if numInfluences > 0 then Infs[numInfluences] = VRawBoneInfluence () -- preallocate
					for i = 1 to numInfluences do Infs[i] = ReadVRawBoneInfluence file
				)

			-- additional UV set
			"EXTRAUV0":
				(
					numUVVerts = hdr.DataCount
					if (numUVVerts != numWedges) then ErrorMessage("Bad vertex count for extra UV set")
					local UV = #()
					UV[numUVVerts] = [ 0, 0 ]
					for i = 1 to numUVVerts do UV[i] = ReadVector2 file
					extraUV[numTexCoords] = UV
					numTexCoords = numTexCoords + 1
				)

			default:
				(
					-- skip unknown chunk
					format "Unknown chunk header: \"%\" at %\n" hdr.ChunkID (ftell file)
					fseek file (hdr.DataSize * hdr.DataCount) #seek_cur
				)
			)
		)
	)
	catch
	(
		fclose file
		messageBox("Error loading file " + filename)
		format "FATAL ERROR: %\n" (getCurrentException())
		return undefined
	)

	format "Read mesh: % verts, % wedges, % tris, % materials, % bones, % influences\n" \
		numVerts numWedges numTris numMaterials numBones numInfluences
	fclose file

	--------- File is completely read now ---------

	-- generate skeleton
	MaxBones = #()
	local rotMatrix = matrix3 1
	for i = 1 to numBones do
	(
		bn = MeshBones[i]
		-- build bone matrix
		q = bn.Orientation
		if ((i == 1) and not g_dontConjugateRoot) then q = conjugate q
		mat = (normalize q) as matrix3
		mat.row4 = bn.Position * g_meshScale
		-- transform from parent bone coordinate space to world space
		if (i > 1) then
		(
			bn.Matrix = mat * MeshBones[bn.ParentIndex + 1].Matrix
		)
		else
		(
			bn.Matrix = mat
		)

		-- get bone length (just for visual appearance)
		childBone = axFindFirstChild MeshBones i
		if (childBone != undefined) then
		(
			len = (length childBone.Position) * g_meshScale
		)
		else
		(
			len = 4		-- no children, default length; note: when len = 1 has bugs with these bones!
		)
		if len < 4 then len = 4
		-- create Max bone
		newBone = getNodeByName bn.Name exact:true ignoreCase:true
		if (newBone == undefined) then
		(
			if (g_reorientBones == false or childBone == undefined) then
			(
				-- create new bone
				newBone = bonesys.createbone	\
					  bn.Matrix.row4			\
					  (bn.Matrix.row4 + len * (normalize bn.Matrix.row1)) \
					  (normalize bn.Matrix.row3)
			)
			else
			(
				-- reorient bone matrix to point directly to a child
				-- get world position of the child bone
				local childPos = childBone.Position * bn.Matrix * g_meshScale
				newBone = bonesys.createbone	\
					  bn.Matrix.row4			\
					  childPos					\
					  bn.Matrix.row3
			)
			newBone.name   = bn.Name
			newBone.width  = g_boneSize
			newBone.height = g_boneSize
--			newBone.setBoneEnable false 0		-- disabled 20.07.2015
			newBone.pos.controller      = TCB_position ()
			newBone.rotation.controller = TCB_rotation ()	-- required for correct animation
			-- setup parent
			if (i > 1) then
			(
				if (bn.ParentIndex >= i) then
				(
					format "Invalid parent % for bone % (%)" bn.ParentIndex (i-1) bn.Name
					return undefined
				)
				newBone.parent = MaxBones[bn.ParentIndex + 1]
			)
			-- store bind pose in custom data block
			custAttributes.add newBone AXBoneCustomDataDef
			mat = (normalize q) as matrix3		-- rebuild 'mat', but without scale
			mat.row4 = bn.Position
			newBone.AX_RelMatrix   = mat
			newBone.AX_WorldMatrix = bn.Matrix
		)
		else
		(
			-- bone already exists
			if g_reposBones then newBone.transform = bn.Matrix
		)
		MaxBones[i] = newBone
	)

	-- generate mesh
	MaxFaces = #()
	MaxVerts = #()
	MaxFaces[numTris]   = [ 0, 0, 0 ]			-- preallocate
	MaxVerts[numWedges] = [ 0, 0, 0 ]			-- ...
	VertList = #();								-- list of wedges linked for each vertex
	VertList.count = numVerts					-- preallocate
	for i = 1 to numVerts do VertList[i] = #()	-- initialize with empty array
	for i = 1 to numWedges do
	(
		local vertId = Wedges[i].PointIndex + 1
		MaxVerts[i] = Verts[vertId] * g_meshScale
		append VertList[vertId] i
	)
	for i = 1 to numTris do
	(
		tri = Tris[i]
		w0 = tri.Wedge0
		w1 = tri.Wedge1
		w2 = tri.Wedge2
		MaxFaces[i] = [ w1+1, w0+1, w2+1 ]		-- note: reversing vertex order
	)
	newMesh = mesh vertices:MaxVerts faces:MaxFaces name:(getFilenameFile filename)
	-- texturing
	newMesh.xray = g_seeThru
	meshop.setNumMaps newMesh (numTexCoords+1)	-- 0 is vertex color, 1+ are textures
	meshop.setMapSupport newMesh 1 true			-- enable texturemap channel
	meshop.setNumMapVerts newMesh 1 numWedges	-- set number of texture vertices
	for i = 1 to numWedges do
	(
		-- set texture coordinates
		w = Wedges[i]
		meshop.setMapVert newMesh 1 i [ w.U, 1-w.V, 1-w.V ]	-- V coordinate is flipped
	)
	for i = 1 to numTris do
	(
		-- setup face vertices and material
		tri = Tris[i]
		meshop.setMapFace newMesh 1 i [ tri.Wedge1+1, tri.Wedge0+1, tri.Wedge2+1 ]
		setFaceMatId newMesh i (tri.MatIndex+1)
	)
	-- extra UV sets (code is similar to above!)
	for j = 2 to numTexCoords do
	(
		format "Loading UV set #% ...\n" j
		uvSet = extraUV[j-1]						-- extraUV does not holds 1st UV set
		meshop.setMapSupport newMesh j true			-- enable texturemap channel
		meshop.setNumMapVerts newMesh j numWedges	-- set number of texture vertices
		for i = 1 to numWedges do
		(
			-- set texture coordinates
			uv = uvSet[i]
			meshop.setMapVert newMesh j i [ uv.x, 1-uv.y, 1-uv.y ]	-- V coordinate is flipped
		)
/* commented 18.04.2014 - probably this is not needed, because completely duplicates code for 1st UV set
		for i = 1 to numTris do
		(
			-- setup face vertices and material
			tri = Tris[i]
			meshop.setMapFace newMesh j i [ tri.Wedge1+1, tri.Wedge0+1, tri.Wedge2+1 ]
			setFaceMatId newMesh i (tri.MatIndex+1)
		)
*/
	)

	newMat = multiMaterial numsubs:numMaterials
	if g_skelOnly then numMaterials = 0		-- do not load materials for this option
	for i = 1 to numMaterials do
	(
		local texDir
		if g_texDir != "" then
		(
			texDir = g_texDir
		)
		else
		(
			texDir = getFilenamePath filename
		)
		local subMat = axImportMaterial Materials[i].MaterialName texDir
		newMat.materialList[i] = subMat
		showTextureMap subMat true
--		format "Material[%] = %\n" i Materials[i].MaterialName
	)
	newMesh.material = newMat

	update newMesh

	-- smooth vertex normals accross UV seams
	max modify mode
	select newMesh

	normalMod = editNormals ()
	addModifier newMesh normalMod
	normalMod.selectBy = 1

	for i = 1 to VertList.count do
	(
		if VertList[i].count > 1 then
		(
			local seamWedges = VertList[i] as bitArray
			local n = #{}
			normalMod.ConvertVertexSelection &seamWedges &n
			normalMod.Average selection:n
		)
	)
	VertList.count = 0
	collapsestack newMesh

	-- generate skin modifier
	skinMod = skin ()
	boneIDMap = #()
	if numBones > 0 then
	(
		addModifier newMesh skinMod
		for i = 1 to numBones do
		(
			if i != numBones then
				skinOps.addBone skinMod MaxBones[i] 0
			else
				skinOps.addBone skinMod MaxBones[i] 1
		)
		-- In Max 2013 the bone IDs are scrambled, so we look them up
		-- by bone's name and stores them in a table.
		local numSkinBones = skinOps.GetNumberBones skinMod
		-- iterate all bones in the Max (could be more than in a mesh)
		for i = 1 to numSkinBones do
		(
			local boneName = skinOps.GetBoneName skinMod i 0
			-- compare with mesh bones by name
			for j = 1 to numBones do
			(
				if boneName == MeshBones[j].Name then
				(
					boneIDMap[j] = i
--					format "MaxID[%]: %, OriginalID: %\n" i boneName j
					j = numBones + 1 -- break the loop (faster than 'exit')
				)
			)
		)
	)

	if skelOnly then
	(
		delete newMesh		-- non-optimal way, may skip mesh creation
		return undefined
	)
	if numBones <= 0 then
	(
		return undefined
	)

--	redrawViews()

	modPanel.setCurrentObject skinMod

	-- setup vertex influences (weights)
	qsort Infs InfluenceSort

	-- build vertex to influence map
	vertInfStart = #()
	vertInfNum   = #()
	vertInfStart[numVerts] = 0		-- preallocate
	vertInfNum[numVerts]   = 0		-- ...
	count = 0
	for i = 1 to numInfluences do
	(
		v     = Infs[i]
		vert  = v.PointIndex+1
		count += 1
		if (i == numInfluences) or (Infs[i+1].PointIndex+1 != vert) then
		(
			-- flush
			vertInfStart[vert] = i - count + 1
			vertInfNum[vert]   = count
			count = 0
		)
	)

--	progressStart "Setting weights ..." -- shouldn't call progress functions, causes crash in script
	disableSceneRedraw()
	numRepairedVerts = 0
	numBadVerts = 0
	try
	(

		for wedge = 1 to numWedges do
		(
			vert    = Wedges[wedge].PointIndex+1
			start   = vertInfStart[vert]
			numInfs = vertInfNum[vert]

/*
			-- This code uses SetVertexWeights
			oldBone = skinOps.GetVertexWeightBoneID skinMod wedge 1
			numWeights = skinOps.GetVertexWeightCount skinMod wedge
			if numWeights > 1 then
			(
				skinOps.ReplaceVertexWeights skinMod wedge oldBone 1
			)
			for i = 1 to numInfs do
			(
				v = Infs[start + i - 1]
				b = boneIDMap[v.BoneIndex+1]
--				format "Inf %(%) % : %\n" wedge vert MeshBones[b].Name v.Weight
				skinOps.SetVertexWeights skinMod wedge b v.Weight
				if b == oldBone then
				(
					oldBone = -1
				)
			)
			if oldBone > 0 then
			(
				skinOps.SetVertexWeights skinMod wedge oldBone 0
			)
*/
			-- This code uses ReplaceVertexWeights with arrays, a few times slower;
			-- it is still here in a case of bugs with SetVertexWeights path
			infBones   = #()
			infWeights = #()
			for i = 1 to numInfs do
			(
				v = Infs[start + i - 1]
				append infBones   boneIDMap[v.BoneIndex + 1]
				append infWeights v.Weight
			)
			skinOps.ReplaceVertexWeights skinMod wedge infBones infWeights
			-- NOTE: older Max versions after ReplaceVertexWeights call performed reset of infBones and
			-- infWeights arrays, so we wasn't able to reuse them. At least Max 2015 doesn't do that.

			-- Check is weights were set correctly
			numWeights = skinOps.GetVertexWeightCount skinMod wedge
			if numWeights != numInfs then
			(
				-- We've tried to set weights for this vertex, but MaxScript decided to keep
				-- other bones as dependency (bug in ReplaceVertexWeights). Try to repair:
				-- enumerate all current weights and set unwanted bone weights to 0 explicitly.
				-- Note: it looks like this is not an issue for Max 2014, it appears in 2015:
				-- https://trello.com/c/76npwkAY/115-possible-bug-with-importer-on-max-2015
--				format "Bad vertex: % bones(%) but %\n" wedge numInfs numWeights
				for w = 1 to numWeights do
				(
					bone = skinOps.GetVertexWeightBoneID skinMod wedge w
					found = findItem infBones bone
					if found == 0 then
					(
						append infBones bone
						append infWeights 0
					)
				)
				skinOps.ReplaceVertexWeights skinMod wedge infBones infWeights
				numWeights = skinOps.GetVertexWeightCount skinMod wedge
				if numWeights != numInfs then
				(
--					format "Bad vertex: %: bones(%) weights(%)\n" wedge infBones infWeights
					numBadVerts += 1
				)
				else
				(
					numRepairedVerts += 1
				)
			)
--			progressUpdate (100.0 * wedge / numWedges)
		)
	)
	catch
	(
		enableSceneRedraw()
--		progressEnd()
		throw()
	)
	enableSceneRedraw()
--	progressEnd()
	if (numRepairedVerts > 0) or (numBadVerts > 0) then
	(
		format "Problems during skinning: % bad vertices, % repaired vertices\n" numBadVerts numRepairedVerts
	)

	-- apply mesh rotation
	if numBones >= 1 then
	(
		MaxBones[1].transform = MaxBones[1].transform * axGetRootMatrix()
	)

	local profileEndTime = timeStamp()
	format "Loaded in % sec\n" ((profileEndTime - profileStartTime) / 1000.0)

	gc()
)


-------------------------------------------------------------------------------
--	Loading PSA file
-------------------------------------------------------------------------------

fn FindPsaTrackIndex Anims Name =
(
	local notfound = true, res = -1
	for i = 1 to Anims.count while notfound do
	(
		if Anims[i].Name == Name then
		(
			res = i
			notfound = false
		)
	)
	res
)

fn FindPsaBoneIndex Bones Name =
(
	local notfound = true, res = -1
	for i = 1 to Bones.count while notfound do
	(
		if Bones[i].Name == Name then
		(
			res = i
			notfound = false
		)
	)
	res
)

-- UseAnimTranslation[] is array of flags signalling that particular bone should use translation
-- from the animation; when value is set to false, mesh translation will be used
fn LoadPsaConfig filename Anims Bones UseAnimTranslation AnimFlags =
(
	-- allocate and initialize UseAnimTranslation array
	UseAnimTranslation[Bones.count] = true			-- preallocate
	for i = 1 to Bones.count do UseAnimTranslation[i] = true
	-- root bone is always translated, start with index 2 below

	case g_animTransMode of
	(
--	1: - use from AnimSet, do nothing here
	2:	(
			for i = 2 to Bones.count do UseAnimTranslation[i] = false
			return undefined
		)
	3:	(
			for i = 2 to Bones.count do UseAnimTranslation[i] = true	-- old behaviour - everything will be taken from the animation
			return undefined
		)
	)

	-- read configuration file
	local cfgFile = openFile filename
	if cfgFile == undefined then return undefined

	local mode = 0

	while eof cfgFile == false do
	(
		local line = readline cfgFile

		-- process directove
		case line of
		(
		"": continue		-- empty line
		"[AnimSet]": ( mode = 1; continue )
		"[UseTranslationBoneNames]": ( mode = 2; continue )
		"[ForceMeshTranslationBoneNames]": ( mode = 3; continue )
		"[RemoveTracks]":
			(
				mode = 4
				-- allocate AnimFlags array, usually not required (currently used for UC2 animations only)
				local numKeys = Anims.count * Bones.count
				AnimFlags[numKeys] = 0		-- preallocate
				for i = 1 to numKeys do AnimFlags[i] = 0
				continue
			)
		)

		-- process ordinary line
		case mode of
		(
		0:	ErrorMessage("unexpected \"" + line + "\"")

		-- AnimSet
		1:	(
				--!! ugly parsing ... but no other params yet
				if line == "bAnimRotationOnly=1" then
				(
					for i = 2 to Bones.count do UseAnimTranslation[i] = false
				)
				else if line == "bAnimRotationOnly=0" then
				(
					-- already set to true
				)
				else
				(
					ErrorMessage("unexpected AnimSet instruction \"" + line + "\"")
				)
			)

		-- UseTranslationBoneNames - use translation from animation, useful with bAnimRotationOnly=true only
		2:	(
				local BoneIndex = FindPsaBoneIndex Bones line
				if BoneIndex > 0 then
				(
					UseAnimTranslation[BoneIndex] = true
				)
				else
				(
					format "WARNING: UseTranslationBoneNames has specified unknown bone \"%\"\n" line
				)
			)

		-- ForceMeshTranslationBoneNames - use translation from mesh
		3:	(
				local BoneIndex = FindPsaBoneIndex Bones line
				if BoneIndex > 0 then
				(
					UseAnimTranslation[BoneIndex] = false
				)
				else
				(
					format "WARNING: ForceMeshTranslationBoneNames has specified unknown bone \"%\"\n" line
				)
			)

		-- RemoveTracks
		4:	(
				-- line is in format "SequenceName.BoneIndex=[trans|rot|all]"
				local tok1 = filterString line "="			-- [1] = SequenceName.BoneIndex, [2] = Flags
				local tok2 = filterString tok1[1] "."		-- [1] = SequenceName, [2] = BoneIndex
				local SeqName    = TrimSpaces(tok2[1])
				local BoneIdxStr = TrimSpaces(tok2[2])
				local Flag       = TrimSpaces(tok1[2])
				local SeqIdx = FindPsaTrackIndex Anims SeqName		--?? can cache this value
				if SeqIdx <= 0 then ErrorMessage("Animation \"" + SeqName + "\" does not exists" + "\nline:" + line)
				FlagIndex = (SeqIdx - 1) * Bones.count + (BoneIdxStr as integer) + 1
				if Flag == "trans" then
				(
					AnimFlags[FlagIndex] = 1	-- NO_TRANSLATION
				)
				else if Flag == "rot" then
				(
					AnimFlags[FlagIndex] = 2	-- NO_ROTATION
				)
				else if Flag == "all" then
				(
					AnimFlags[FlagIndex] = 3	-- NO_TRANSLATION | NO_ROTATION
				)
				else
				(
					ErrorMessage("unknown RemoveTracks flag \"" + Flag + "\"")
				)
			)

		default:
			ErrorMessage("unexpected config error")
		)
	)
	close cfgFile
)


fn ImportPsaFile filename trackNum all:false =
(
	local Bones     = #()
	      Anims     = #()

	local UseAnimTranslation = #()
	local AnimFlags          = #()

	local numBones  = 0
	local numAnims  = 0

	local keyPos = 0

	--------- Read the file ---------

	try
	(
		file = fopen filename "rb"
		if file == undefined then return undefined

		-- First header --
		hdr = ReadChunkHeader file
		if (hdr.ChunkID != "ANIMHEAD") then
		(
			ErrorMessage("Bad chunk header: \"" + hdr.ChunkID + "\"")
		)

		while not IsEndOfFile(file) do
		(
			hdr = ReadChunkHeader file
--			format "Chunk: % (% items, % bytes/item, pos %)\n" hdr.ChunkID hdr.DataCount hdr.DataSize (ftell file)
			case hdr.ChunkID of
			(
			-- Bone links --
			"BONENAMES":
				(
					numBones = hdr.DataCount
					if numBones > 0 then Bones[numBones] = VBone ()		-- preallocate
					for i = 1 to numBones do Bones[i] = ReadVBone file
				)

			-- Animation sequence info --
			"ANIMINFO":
				(
					numAnims = hdr.DataCount
					if numAnims > 0 then Anims[numAnims] = AnimInfoBinary ()	-- preallocate
					for i = 1 to numAnims do Anims[i] = ReadAnimInfoBinary file

					if trackNum < 0 then
					(
						-- information only
						fclose file
						return undefined
					)
				)

			-- Key data --
			"ANIMKEYS":
				(
					-- determine chunk of the file to load later
					if all then trackNum = 1
					keyPos = ftell file
					for i = 1 to trackNum - 1 do
						keyPos += Anims[i].NumRawFrames * numBones * 32
					if all then
						numFrames = hdr.DataCount / Bones.count
					else
						numFrames = Anims[trackNum].NumRawFrames
					-- skip this chunk
					fseek file (hdr.DataSize * hdr.DataCount) #seek_cur
				)

			default:
				(
					-- skip unknown chunk
					format "Unknown chunk header: \"%\" at %\n" hdr.ChunkID (ftell file)
					fseek file (hdr.DataSize * hdr.DataCount) #seek_cur
				)
			)
		)
	)
	catch
	(
		fclose file
		messageBox ("Error loading file " + filename)
		format "FATAL ERROR: %\n" (getCurrentException())
		throw()
		return undefined
	)

	if numBones < 1 then
	(
		format "Animations has no bones\n"
		return undefined
	)

	if keyPos == 0 then
	(
		format "No ANIMKEYS chunk was found\n"
		return undefined
	)

	-- find existing scene bones
	MaxBones     = #()
	BindPoseInfo = #()
	SceneBones = FindAllBones()
	for i = 1 to numBones do
	(
		boneName = Bones[i].Name
		local notfound = true
		for j = 1 to SceneBones.count while notfound do
		(
			b = SceneBones[j]
			if b.name == boneName then
			(
				MaxBones[i]     = b
				BindPoseInfo[i] = custAttributes.get b AXBoneCustomDataDef	-- could be 'undefined'
				notfound        = false
			)
		)

		if notfound then
		(
			format "WARNING: cannot find bone %\n" boneName
		)
		else if BindPoseInfo[i] == undefined then
		(
			format "WARNING: cannot get bind pose information for bone %\n" boneName
		)
	)

	-- verify for found root bone
	if MaxBones[1] == undefined then
	(
		messageBox ("WARNING: Unable to find root bone \"" + Bones[1].Name + "\"\nAnimation may appear incorrectly!")
	)

	set coordsys world
	startframe = 0	-- can modify layer ...
	if g_animAtSlider then
	(
		startframe = sliderTime
	)
	else
	(
		RemoveAnimation()
	)

	LoadPsaConfig ( (getFilenamePath filename) + (getFilenameFile filename) + ".config" ) Anims Bones UseAnimTranslation AnimFlags

/*
	format "[% trans % flags]\n" UseAnimTranslation.count AnimFlags.count
	for i = 1 to UseAnimTranslation.count do
	(
		if UseAnimTranslation[i] then format "trans: % %\n" i Bones[i].Name
	)
*/

	format "Loading track % (%), % keys\n" trackNum Anims[trackNum].Name (numFrames * Bones.count)
	firstFrame = #()
	firstFlag  = (trackNum - 1) * numBones + 1
	flagCount  = AnimFlags.count
	fseek file keyPos #seek_set							-- seek to animation keys

	animate on
	(
		progressStart "Loading animation ..."
		for i = 1 to numFrames do
		(
			at time (startframe + i - 1)
			(
				flagIndex = firstFlag
				for b = 1 to Bones.count do
				(
					-- get key
					k = ReadVQuatAnimKey file				-- read key from file
					-- get bones
					bone     = MaxBones[b]					-- scene bone to transform
					BindPose = BindPoseInfo[b]				-- for BindPose transform
					-- get animation flags
					flag = 0
					if flagIndex < flagCount then flag = AnimFlags[flagIndex]
					flagIndex = flagIndex + 1
					-- when either scene or mesh bone is missing, skip everything (key was already read)
					if bone == undefined then continue

					local mat
					if BindPose != undefined then
					(
						-- rotation
						if (bit.and flag 2) != 0 then		-- NO_ROTATION
						(
							-- rotation from mesh
							mat = BindPose.AX_RelMatrix
						)
						else
						(
							-- rotation from animation
							q = k.Orientation
							if ((b == 1) and not g_dontConjugateRoot) then q = conjugate q
							mat = (q as matrix3)
						)
						-- translation
						if (bit.and flag 1) != 0 then		-- NO_TRANSLATION
						(
							-- translation from the mesh
							mat.row4 = BindPose.AX_RelMatrix.row4 * g_meshScale
						)
						else if not UseAnimTranslation[b] then
						(
							-- translation from the mesh
							mat.row4 = BindPose.AX_RelMatrix.row4 * g_meshScale
						)
						else
						(
							-- translation from animation
							mat.row4 = k.Position * g_meshScale
						)
					)
					else
					(
						-- the BindPose object doesn't exists, use all data from the animation
						q = k.Orientation					-- rotation from animation
						p = k.Position * g_meshScale		-- translation from animation
						-- build matrix
						if ((b == 1) and not g_dontConjugateRoot) then q = conjugate q
						-- build matrix
						mat = (q as matrix3)
						mat.row4 = p
					)

					-- modify bone
					if bone.parent != undefined then
					(
						bone.transform = mat * bone.parent.transform
					)
					else
					(
						bone.transform = mat
					)
					-- remember 1st frame
					if (i == 1) then firstFrame[b] = bone.transform
				)
				-- rotate animation
				if MaxBones[1] != undefined then
				(
					MaxBones[1].transform = MaxBones[1].transform * axGetRootMatrix()
				)
			)
			-- progress bar
			progressUpdate (100.0 * i / numFrames)
			if getProgressCancel() then exit
		)
		if g_fixLooping then
		(
			-- Add extra 2 frames for correct TCB controller work.
			-- The second frame is not necessary if there is no keys after last frame
			-- (may purge all keys before animation loading instead of adding 2nd key)
			for i = 0 to 1 do
			(
				at time (startframe + numFrames + i)
				for b = 1 to Bones.count do
				(
					bone = MaxBones[b]
					if bone != undefined then
					(
						bone.transform = firstFrame[b]
					)
				)
			)
		)
		progressEnd()
	)

	-- finish loading
	fclose file

	sliderTime = 1
	extraFrame = 0
	if g_fixLooping then extraFrame = 1

	if g_updateTime then
	(
		ar_start = startframe
		ar_end   = startframe + numFrames - 1 + extraFrame
	)
	else
	(
		ar_start = animationRange.start.frame
		ar_end   = animationRange.end.frame
		if animationRange.start.frame > startframe then
			ar_start = startframe
		if animationRange.end.frame < startframe + numFrames + extraFrame then
			ar_end   = startframe + numFrames - 1 + extraFrame
	)
	if (ar_end == ar_start) then ar_end = ar_end + 1 -- avoid zero-length intervals

	animationRange = interval ar_start ar_end
	sliderTime     = startframe
--	frameRate      = track.AnimRate

	if g_playAnim then playAnimation immediateReturn:true

	gc()
)


-------------------------------------------------------------------------------
--	User interface
-------------------------------------------------------------------------------

-- layout
global axRolloutList
global axRolloutStates
global g_axScrollPos

fn axStoreLayout roll =
(
	if axRolloutStates == undefined then axRolloutStates = #()
	for i = 1 to axRolloutList.count do
	(
		axRolloutStates[i] = axRolloutList[i].open
	)
	-- sometimes 'roll' is non-null, but it's property 'scrollPos' is inaccessible
	if roll.scrollPos != undefined then g_axScrollPos = roll.scrollPos
)


fn axRestoreLayout roll =
(
	if axRolloutStates != undefined then
	(
		for i = 1 to axRolloutList.count do
		(
			axRolloutList[i].open = axRolloutStates[i]
		)
	)
	-- when execing first time, layout will not be stored, and g_axScrollPos will be undefined
	if g_axScrollPos != undefined then roll.scrollPos = g_axScrollPos
)


global MeshFileName
global AnimFileName

fn axLoadAnimation index =
(
	if (index > 0) and (index <= Anims.count) then ImportPsaFile AnimFileName index
)


rollout axInfoRollout "ActorX Importer"
(
	-- copyright label
	label     Lbl1 "Version 1.33"
	label     Lbl2 "\xA9 2009-2015 Konstantin Nosov (Gildor)"
	hyperlink Lbl3 "http://www.gildor.org/" \
					address:"http://www.gildor.org/projects/unactorx" align:#center \
					color:black hovercolor:blue visitedcolor:black

	on axInfoRollout close do
	(
		format "Saving settings ...\n"
		axSerializeSettings false
		axStoreLayout axInfoRollout
	)
)


rollout axMeshImportRollout "Mesh Import"
(
	checkbox ChkSeeThru    "See-Thru Mesh" checked:g_seeThru
	checkbox ChkSkelOnly   "Load skeleton only" checked:g_skelOnly
	button   BtnImportPsk  "Import PSK ..."

	-- event handlers

	on ChkSeeThru    changed state do g_seeThru    = state
	on ChkSkelOnly   changed state do g_skelOnly   = state

	on BtnImportPsk pressed do
	(
		local filename = getOpenFileName types:"ActorX Mesh (*.psk,*.pskx)|*.psk;*.pskx|All (*.*)|*.*|" filename:g_lastDir1
		if filename != undefined then
		(
			MeshFileName = filename
			g_lastDir1 = getFilenamePath MeshFileName
			if DoesFileExist MeshFileName then ImportPskFile MeshFileName skelOnly:g_skelOnly
		)
	)
)


rollout axAnimImportRollout "Animation Import"
(
	Group "Animation Import"
	(
		button   BtnImportPsa  "Import PSA ..."
		listbox  LstAnims      "Animations:"     height:13
		checkbox ChkAnimTime   "Update animation length" checked:g_updateTime
		checkbox ChkFixLooping "Fix loop animation" checked:g_fixLooping tooltip:"Append 1st keyframe to animation\ntrack for smooth loop"
		checkbox ChkPlayAnim   "Play animation" checked:g_playAnim
		checkbox ChkAtSlider   "Import at slider position" checked:g_animAtSlider
		dropdownlist LstTransMode "Translation mode" items:#("Use from AnimSet", "Force mesh translation", "Force AnimSet translation") selection:g_animTransMode
		button   BtnImportTrk  "Load track" across:2
		button   BtnImportAll  "Load all" tooltip:"Load all animations as a single track"
	)

	-- event handlers

	on BtnImportPsa pressed do
	(
		local filename = getOpenFileName types:"ActorX Animation (*.psa)|*.psa|All (*.*)|*.*|" filename:g_lastDir2

		if filename != undefined then
		(
			AnimFileName = filename
			g_lastDir2 = getFilenamePath AnimFileName
			if DoesFileExist AnimFileName then
			(
				ImportPsaFile AnimFileName -1
				LstAnims.items = for a in Anims collect (a.Name + " [" + (a.NumRawFrames as string) + "]")
			)
		)
	)

	on BtnImportTrk pressed       do axLoadAnimation LstAnims.selection
	on BtnImportAll pressed       do ImportPsaFile AnimFileName 1 all:true
	on LstAnims doubleClicked sel do axLoadAnimation sel

	on ChkAnimTime   changed state do g_updateTime    = state
	on ChkFixLooping changed state do g_fixLooping    = state
	on ChkPlayAnim   changed state do g_playAnim      = state
	on ChkAtSlider   changed state do g_animAtSlider  = state
	on LstTransMode  selected mode do g_animTransMode = mode

	on axAnimImportRollout open do
	(
		-- fill LstAnims
		LstAnims.items = for a in Anims collect (a.Name + " [" + (a.NumRawFrames as string) + "]")
	)
)


rollout axTexturesRollout "Materials"
(
	edittext EdTexPath     "Path to materials" text:g_texDir width:180 across:2
	button   BtnBrowseTex  "..."     align:#right height:16
	checkbox ChkTexRecurse "Recurse" checked:g_texRecurse
	label    LblMissingTex "On missing texture:" across:2
	radiobuttons RadMissingTex labels:#("do nothing", "ask") default:g_texMissAction align:#left columns:1

	on EdTexPath    changed val do g_texDir = val
	on BtnBrowseTex pressed do
	(
		dir = getSavePath caption:"Directory for texture lookup" initialDir:g_texDir
		if dir != undefined then
		(
			g_texDir       = dir
			EdTexPath.text = dir
		)
	)

	on ChkTexRecurse changed state do g_texRecurse    = state
	on RadMissingTex changed state do g_texMissAction = state
)


rollout axToolsRollout "Tools"
(
	button BtnReset "Reset to defaults" width:180

	button BtnRestoreBindpose "Restore BindPose" width:180
	button BtnRemoveAnimation "Remove animation" width:180
	button BtnClearScene      "Clear scene"      width:180
	button BtnBatchExport     "Batch export"     width:180
	button BtnReloadScript    "Reload importer"  width:180

	on BtnReset pressed do
	(
		if configFile != undefined then deleteFile configFile
		axDefaultSettings()
		-- reset controls
		axShowUI()
	)
	on BtnRestoreBindpose pressed do RestoreBindpose()
	on BtnRemoveAnimation pressed do RemoveAnimation()
	on BtnClearScene      pressed do ClearMaxScene()
	on BtnBatchExport     pressed do fileIn "export_fbx.ms"
	on BtnReloadScript    pressed do
	(
		if getSourceFileName != undefined then	-- checking Max version (Max9+) ...
		(
			axStoreLayout axInfoRollout
			fileIn(getSourceFileName())
		)
	)
)


rollout axSettingsRollout "Mesh Settings"
(
	spinner SpnBoneSize  "Bone size"  range:[0.1,10,g_boneSize]     type:#float scale:0.1  align:#left  across:2
	spinner SpnMeshScale "Mesh scale" range:[0.01,1000,g_meshScale] type:#float scale:0.01 align:#right
	checkbox ChkRepBones "Reposition existing bones" checked:g_reposBones

	group "Mesh rotation"
	(
		spinner  SpnRY "Yaw"   range:[-180,180,g_rotY] type:#integer scale:90 fieldwidth:35 align:#left across:3
		spinner  SpnRP "Pitch" range:[-180,180,g_rotP] type:#integer scale:90 fieldwidth:35
		spinner  SpnRR "Roll"  range:[-180,180,g_rotR] type:#integer scale:90 fieldwidth:35 align:#right
		button   BtnRotMaya    "Maya" across:3
		button   BtnRotReset   "Reset"
		button   BtnRotApply   "Apply"
	)

	group "Mesh offset"
	(
		spinner  SpnTX "X"     range:[-10000,10000,g_transX] type:#float scale:0.01 fieldwidth:50 align:#left across:3
		spinner  SpnTY "Y"     range:[-10000,10000,g_transY] type:#float scale:0.01 fieldwidth:50
		spinner  SpnTZ "Z"     range:[-10000,10000,g_transZ] type:#float scale:0.01 fieldwidth:50 align:#right
	)

	-- event handlers
	on SpnBoneSize  changed val do g_boneSize  = val
	on SpnMeshScale changed val do g_meshScale = val
	on ChkRepBones  changed state do g_reposBones = state

	on SpnRY changed val do g_rotY = val
	on SpnRP changed val do g_rotP = val
	on SpnRR changed val do g_rotR = val
	on SpnTX changed val do g_transX = val
	on SpnTY changed val do g_transY = val
	on SpnTZ changed val do g_transZ = val

	on BtnRotMaya pressed do
	(
		g_rotY = SpnRY.value = -90
		g_rotP = SpnRP.value =   0
		g_rotR = SpnRR.value =  90
		RestoreBindpose()
	)
	on BtnRotReset pressed do
	(
		g_rotY = SpnRY.value = 0
		g_rotP = SpnRP.value = 0
		g_rotR = SpnRR.value = 0
		RestoreBindpose()
	)
	on BtnRotApply pressed do RestoreBindpose()
)


rollout axAdvSettingsRollout "Advanced Settings"
(
	label Lbl1                "WARNING: do not modify these settings"
	label Lbl2                "unless you know what you are doing!"
	checkbox ChkReorientBones "Reorient bones" checked:g_reorientBones
	checkbox ChkDontConjRoot  "Don't conjugate root bone" checked:g_dontConjugateRoot

	-- event handlers
	on ChkReorientBones changed state do g_reorientBones = state
	on ChkDontConjRoot changed state do g_dontConjugateRoot = state
)


global axImportFloater

fn axShowUI =
(
	-- request position of previous window, if it was already opened
	local x = 30
	local y = 100
	local w = 250
	local h = 700
	if axImportFloater != undefined then
	(
		x = axImportFloater.pos.x
		y = axImportFloater.pos.y
		w = axImportFloater.size.x
		h = axImportFloater.size.y
		-- close old window
		closeRolloutFloater axImportFloater
	)
	-- Create plugin window
	axImportFloater = newRolloutFloater "ActorX Import" w h x y				-- create a new window

	-- init axRolloutList
	axRolloutList = #(axInfoRollout, axMeshImportRollout, axAnimImportRollout, axTexturesRollout, axToolsRollout, axSettingsRollout, axAdvSettingsRollout)

	-- add controls
	for i = 1 to axRolloutList.count do
	(
		addRollout axRolloutList[i] axImportFloater
	)

	axRestoreLayout axInfoRollout
)


-------------------------------------------------------------------------------
--	Plugin startup
-------------------------------------------------------------------------------

global g_axImporterVersion

if (g_axImporterVersion == undefined) then
(
	-- initialize plugin
	heapSize += 33554432	-- 32 Mb; will speedup most tasks
	Anims     = #()
	g_axImporterVersion = AX_IMPORTER_VERSION
	axDefaultSettings()
	axSerializeSettings(true)

	if getSourceFileName != undefined then	-- checking Max version (Max9+) ...
	(
		-- Add action handler (macro script).
		-- Max will copy contents of macroScript() block to the "AppData/Local/Autodesk/3dsMax/*/ENU/usermacros".
		-- To avoid copying of entire file we're generating string which will simply execute THIS file.
		str = "macroScript GildorTools_ActorXImporter category:\"Gildor Tools\" buttontext:\"ActorX Importer\" tooltip:\"ActorX Importer\"\n" \
			+ "(\n" \
			+ "    fileIn \"" + getSourceFileName() + "\"\n" \
			+ ")\n"
		execute str
	)
)


if (g_axImporterVersion != AX_IMPORTER_VERSION) then
(
	format "ActorX Importer has been updated while 3ds Max is running.\nReloading settings.\n"
	-- copy-paste of code above
	g_axImporterVersion = AX_IMPORTER_VERSION
	axDefaultSettings()
	axSerializeSettings(true)
)

axShowUI()
